Skip to content

Preserve leading fractional zeros in plural operands - #1342

Open
emme1t wants to merge 1 commit into
python-babel:masterfrom
emme1t:fix/plural-fraction-leading-zeros
Open

emme1t wants to merge 1 commit into
python-babel:masterfrom
emme1t:fix/plural-fraction-leading-zeros

Conversation

@emme1t

@emme1t emme1t commented Sep 12, 2026

Copy link
Copy Markdown

extract_operands() undercounts visible fractional digits for numbers below 1 when the fractional part starts with zeros. For example, Decimal('0.001') currently produces v = 1, w = 1; both operands should be 3 under the CLDR operand definitions.

This affects locale plural selection:

from decimal import Decimal
from babel import Locale

Locale('lv').plural_form(Decimal('0.011'))
# Before: 'zero'
# After:  'one'

The Latvian rule distinguishes v = 2 from v != 2. Decimal.as_tuple().digits omits leading fractional zeros, so the current calculation treats 0.011 like a number with two fractional digits.

Compute v from the decimal exponent and derive w by subtracting the trailing-zero count. Fractional values f and t retain their existing calculation. Tests cover leading zeros, trailing zeros, signed values, decimal zero, a positive exponent, float input, custom plural rules, and the Latvian locale rule.

Validation on Windows / Python 3.13.13 with the repository's CLDR 48.2 import:

  • New regression cases before the fix: 10 failed.
  • python -m pytest tests/test_plural.py -q: 49 passed.
  • python -m pytest -q: 7839 passed, 9 skipped, 2 xfailed.
  • Ruff lint and format checks pass for both changed files; git diff --check passes.

AI assistance: Prepared with Codex. The reproducer and tests listed above were executed locally.

@akx

akx commented Sep 18, 2026

Copy link
Copy Markdown
Member

Is there a related issue, or how did you come across this? Could you show another compliant CLDR implementation producing v = 3, w = 3?

@emme1t

emme1t commented Sep 19, 2026

Copy link
Copy Markdown
Author

Thanks for asking. I don't have a related issue to link. I'm working on a project that automates finding and submitting bug-fix PRs and compatibility issues to GitHub projects with active contributor communities. This PR came out of that workflow, which checked extract_operands() against the CLDR operand definitions and ran local regression tests.

The specific discrepancy is that Decimal('0.001').as_tuple() has digits=(1,) and exponent=-3, so counting the stored fractional digits loses the leading fractional zeros.

I have now independently checked ICU4J 78.1 on Java 21. It produces v=3, w=3 for 0.001. Here is the reproducer, using ICU's number formatter to produce the operands:

import java.math.BigDecimal;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.Precision;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.PluralRules.Operand;
import com.ibm.icu.util.ULocale;

class CheckOperands {
    public static void main(String[] args) {
        var rules = PluralRules.forLocale(new ULocale("lv"));
        for (String s : new String[]{"0.001", "0.00100", "0.011", "0.11", "0.000"}) {
            var input = new BigDecimal(s);
            var formatted = NumberFormatter.withLocale(ULocale.ROOT)
                .precision(Precision.fixedFraction(Math.max(0, input.scale())))
                .format(input);
            var operands = formatted.getFixedDecimal();
            System.out.printf("%s: v=%.0f w=%.0f f=%.0f t=%.0f lv=%s%n",
                s, operands.getPluralOperand(Operand.v),
                operands.getPluralOperand(Operand.w),
                operands.getPluralOperand(Operand.f),
                operands.getPluralOperand(Operand.t), rules.select(formatted));
        }
    }
}

With the ICU4J 78.1 JAR, run java -cp icu4j-78.1.jar CheckOperands.java:

0.001: v=3 w=3 f=1 t=1 lv=one
0.00100: v=5 w=3 f=100 t=1 lv=other
0.011: v=3 w=3 f=11 t=11 lv=one
0.11: v=2 w=2 f=11 t=11 lv=zero
0.000: v=3 w=0 f=0 t=0 lv=zero

The explicit fraction precision preserves the visible digits of the input, including trailing zeros, matching the Decimal inputs in the PR. The operand inspection APIs emit deprecation warnings in ICU4J 78.1; the example above ran successfully. Plural selection uses select(FormattedNumber).

All five operand/category results match this PR. I also reran tests/test_plural.py: 49 passed.

The original investigation, this cross-check, and this reply were prepared with Codex assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants